import pandas as pd
import numpy as np
import category_encoders as ce
import os
import gc
from tqdm import tqdm
# 核心模型使用第三方库
import lightgbm as lgb
import xgboost as xgb  # 导入XGBoost库
from sklearn.ensemble import RandomForestClassifier
# 交叉验证所使用的第三方库
from sklearn.model_selection import StratifiedKFold, KFold
# 评估指标所使用的的第三方库
from sklearn.metrics import roc_auc_score as auc
# 忽略报警所使用的第三方库
from sklearn.metrics import mean_squared_error
import warnings
import matplotlib.pyplot as plt
import seaborn as sns


warnings.filterwarnings('ignore')
pd.set_option('display.max_columns', 1000)
pd.set_option('display.max_row', 1000)

train_data = pd.read_csv("data/train.csv")
test_data = pd.read_csv("data/evaluation_public.csv")

train_data['istest'] = 0
test_data['istest'] = 1

data = pd.concat([train_data, test_data]).reset_index(drop=True)

data['id_by_me'] = pd.Series(range(len(data)))

del data['ip_type']

# 特征工程：根据'op_city'列判断是否为'国外'或'未知'，若是则对应新特征'is_forei/unknown'赋值为1，否则为0
# 这里使用了transform方法结合lambda函数实现条件判断赋值
data['is_forei/unknown'] = data['op_city'].transform(lambda x: 1 if x in ['国外', '未知'] else 0)
# 根据'http_status_code'列判断是否等于200，若等于则'is_fail_code'赋值为0，表示正常状态；否则赋值为1，表示失败状态
data['is_fail_code'] = data['http_status_code'].transform(lambda x: 0 if x == 200 else 1)
# 判断'http_status_code'是否在[500, 502]中，若是则'is_code_5'赋值为1，用于标记特定的错误状态码情况
data['is_code_5'] = data['http_status_code'].apply(lambda x: 1 if x in [500, 502] else 0)
# 根据'url'列判断是否在指定的登录相关网址中，若是则'is_login_url'赋值为1，用于标记是否为登录相关操作
data['is_login_url'] = data['url'].apply(lambda x: 1 if x in ['xxx.com/getVerifyCode', 'xxx.com/getLoginType'] else 0)

# 将'op_datetime'列转换为日期时间类型，方便后续提取时间相关特征
data['op_datetime'] = pd.to_datetime(data['op_datetime'])
# 提取日期中的日部分，格式为"MM-dd"，作为新特征'day'，例如"01-01"
data['day'] = data['op_datetime'].astype(str).apply(lambda x: str(x)[5:10])
# 提取日期中的小时部分，作为新特征'hour'，取值范围是0 - 23
data['hour'] = data['op_datetime'].dt.hour
# 提取日期中的星期几部分（0表示周一，6表示周日），并加1使其范围变为1 - 7，作为新特征'weekday'
data['weekday'] = data['op_datetime'].dt.weekday + 1

# 按照'user_name'和'op_datetime'对数据进行排序，然后重置索引，使得数据按用户操作时间先后顺序排列
data = data.sort_values(by=['user_name', 'op_datetime']).reset_index(drop=True)

# 通过三角函数转换，将小时信息转换为正弦值，生成新特征'hour_sin'，有助于捕捉时间的周期性规律
data['hour_sin'] = np.sin(data['hour'] / 24 * 2 * np.pi)
# 同理，将小时信息转换为余弦值，生成新特征'hour_cos'
data['hour_cos'] = np.cos(data['hour'] / 24 * 2 * np.pi)

# 提取日期中的日部分，格式为"dd"，例如"01"，作为新特征'op_day'
data['op_day'] = data['op_datetime'].astype(str).apply(lambda x: str(x)[8:10])

# 提取日期时间中的分钟部分，作为新特征'min'，取值范围是0 - 59
data['min'] = data['op_datetime'].apply(lambda x: int(str(x)[-5:-3]))
# 通过三角函数转换，将分钟信息转换为正弦值，生成新特征'min_sin'
data['min_sin'] = np.sin(data['min'] / 60 * 2 * np.pi)
# 同理，将分钟信息转换为余弦值，生成新特征'min_cos'
data['min_cos'] = np.cos(data['min'] / 60 * 2 * np.pi)

# 计算每个用户上一次操作与当前操作的时间差（以分钟为单位），生成新特征'diff_last_1'
data['diff_last_1'] = data.groupby('user_name')['op_datetime'].transform(lambda i: i.diff(1)).dt.total_seconds() / 60
# 计算每个用户上上次操作与当前操作的时间差（以分钟为单位），生成新特征'diff_last_2'
data['diff_last_2'] = data.groupby('user_name')['op_datetime'].transform(lambda i: i.diff(2)).dt.total_seconds() / 60

train_data = data[data['istest'] == 0]
test = data[data['istest'] == 1]

# train：
# 计算训练数据中每个用户下一次操作与当前操作的时间差（以分钟为单位），
# 通过对每个用户的'op_datetime'列进行反向差分（diff(-1)）获取时间差，
# 再将时间差的总秒数转换为分钟数，并取相反数（因为是下一次操作与当前操作的时间差，方向与之前的不同）
train_data['diff_next'] = -(
    train_data.groupby('user_name')['op_datetime'].transform(lambda i: i.diff(-1))).dt.total_seconds() / 60
# 将包含'diff_next'和'id_by_me'列的训练数据与整体数据'data'按照'id_by_me'进行左连接，
# 这样'data'中就包含了新计算的'diff_next'特征（对于测试集部分该特征会是缺失值，后续可根据需要处理）
data = pd.merge(data, train_data[['diff_next', 'id_by_me']], how='left', on='id_by_me')

# 定义一个特征列表，包含后续要进行分组统计操作的列名，这些列通常是一些具有分类性质的字段，
# 例如用户名称、部门、IP相关、设备相关、浏览器相关以及操作系统相关等信息
fea = ['user_name', 'department', 'ip_transform', 'device_num_transform', 'browser_version', 'browser',
       'os_type', 'os_version', 'op_city', 'log_system_transform', 'url']

# 针对特征列表中的每一个列，进行以下操作：
# 计算该列分组下'diff_last_1'（每个用户上一次操作与当前操作的时间差）特征的平均值，
# 这里使用了分组变换（transform）操作，它会按照当前列的值进行分组，然后在每个分组内计算平均值，
# 最终结果的形状与原数据'data'的行数一致，每行对应其所在分组的平均时间差
for col in fea:
    data[col + '_diff1_mean'] = data.groupby(col)['diff_last_1'].transform('mean')
    # 计算该列分组下'diff_last_1'特征的标准差，用于衡量每个分组内时间差的离散程度
    data[col + '_diff1_std'] = data.groupby(col)['diff_last_1'].transform('std')
    # 计算该列分组下'diff_last_1'特征的最大值，获取每个分组内时间差的最大情况
    data[col + '_diff1_max'] = data.groupby(col)['diff_last_1'].transform('max')
    # 计算该列分组下'diff_last_1'特征的最小值，获取每个分组内时间差的最小情况
    data[col + '_diff1_min'] = data.groupby(col)['diff_last_1'].transform('min')

# 同样针对特征列表中的每一个列，进行以下关于'diff_next'（每个用户下一次操作与当前操作的时间差）特征的操作：
# 计算该列分组下'diff_next'特征的平均值，反映每个分组内下一次操作时间差的平均情况
for col in fea:
    data[col + '_diff_next_mean'] = data.groupby(col)['diff_next'].transform('mean')
    # 计算该列分组下'diff_next'特征的标准差，衡量每个分组内下一次操作时间差的离散程度
    data[col + '_diff_next_std'] = data.groupby(col)['diff_next'].transform('std')
    # 计算该列分组下'diff_next'特征的最大值，获取每个分组内下一次操作时间差的最大情况
    data[col + '_diff_next_max'] = data.groupby(col)['diff_next'].transform('max')
    # 计算该列分组下'diff_next'特征的最小值，获取每个分组内下一次操作时间差的最小情况
    data[col + '_diff_next_min'] = data.groupby(col)['diff_next'].transform('min')

# data.drop(['browser_version_diff1_min','browser_diff1_min','os_type_diff1_min','os_version_diff1_min'],axis=1,inplace=True)

data = data.fillna(-999)


# 定义一个函数，用于判断输入的参数x（通常是HTTP状态码）是否为200
# 如果是200，则返回0，表示正常状态；如果不是200，则返回1，表示失败状态
def is_fail_code(x):
    if x == 200:
        return 0
    else:
        return 1

# 原本这里可能是想通过调用上述函数来更新'data'数据集中'is_fail_code'列的值，
# 使其根据'http_status_code'列的值来判断是否为失败状态，不过当前这行被注释掉了，可能后续代码有其他处理方式
# data['is_fail_code']=data['http_status_code'].apply(is_fail_code)

# 定义一个新的列'is_fail_usr'，通过对'data'数据集中的'user_name'列应用匿名函数进行赋值
# 如果'user_name'的值为"-999"，则表示登录失败，对应'is_fail_usr'列的值为1；否则为0
data['is_fail_usr'] = data['user_name'].apply(lambda x: 1 if x == "-999" else 0)  # 登陆失败

# 定义一个函数，用于判断输入的参数x（通常是代表星期几的数字，0表示周一，6表示周日）是否小于6
# 如果小于6，说明不是周末，返回0；如果大于等于6，说明是周末，返回1
def isweekend(x):
    if (x < 6):
        return 0
    else:
        return 1

# 通过对'data'数据集中的'weekday'列应用上述'isweekend'函数，来创建一个新列'isweekend'
# 该列用于标记每行数据对应的日期是否为周末，是则值为1，不是则值为0
data['isweekend'] = data['weekday'].apply(isweekend)

# 定义一个函数，用于判断输入的参数x（通常是代表一天中小时数的数字，范围是0 - 23）是否在7（早上）到20（晚上）这个区间内
# 如果在这个区间内，说明是白天，返回0；如果不在这个区间内，说明是夜晚，返回1
def isnight(x):
    if (x > 7) and (x < 20):
        return 0
    else:
        return 1

# 通过对'data'数据集中的'hour'列应用上述'isnight'函数，来创建一个新列'isnight'
# 该列用于标记每行数据对应的时间是否为夜晚，是则值为1，不是则值为0
data['isnight'] = data['hour'].apply(isnight)

# 定义一个列表，包含了一些被认定为节假日放假的日期（这里日期格式为"MM-dd"）
holiday = ['01-31', '02-01', '02-02', '02-03', '02-04', '02-05', '02-06',
           '04-03', '04-04', '04-05', '05-01', '05-02', '05-03', '05-04',
           '06-03', '06-04', '06-05']

# 定义一个函数，用于判断输入的参数x（通常是从日期时间列提取出来的日期部分，格式为"MM-dd"）是否在'holiday'列表中
# 如果在列表中，说明是节假日，返回1；如果不在列表中，说明不是节假日，返回0
def if_holiday(x):
    if x in holiday:
        return 1
    else:
        return 0

# 通过对'data'数据集中的'op_datetime'列先提取出日期部分（格式为"MM-dd"），再应用上述'if_holiday'函数，
# 来创建一个新列'isholiday'，用于标记每行数据对应的日期是否为节假日，是则值为1，不是则值为0
data['isholiday'] = data['op_datetime'].apply(lambda x: if_holiday(str(x)[5:10]))
# 调休
adjust = ['01-29', '01-30', '04-02', '04-24', '05-07']


# 定义一个函数，用于判断输入的参数x是否在名为'adjust'的列表中（虽然代码中未显示'adjust'列表的定义，但推测是一些调休日期相关的内容）
# 如果在列表中，返回1，表示是调休日；如果不在列表中，返回0，表示不是调休日
def if_adjust(x):
    if x in adjust:
        return 1
    else:
        return 0

# 通过对'data'数据集中的'day'列应用上述'if_adjust'函数，创建一个新列'is_adjust'
# 该列用于标记每行数据对应的日期是否为调休日，是则值为1，不是则值为0
data['is_adjust'] = data['day'].apply(if_adjust)

# 创建一个新列'is_not_work'，其逻辑是判断是否为非工作日
# 通过对'isweekend'（是否为周末）、'is_adjust'（是否为调休日）以及'isholiday'（是否为节假日）这几个特征进行逻辑运算来确定
# 具体逻辑为：如果是周末且不是调休日，或者是节假日，那么就是非工作日，对应'is_not_work'列的值为True（转换为布尔类型后参与逻辑运算）
data['is_not_work'] = data['isweekend'].astype(bool) & (~data['is_adjust']) | (data['isholiday'].astype(bool))

# 定义一个包含时间相关特征名称的列表，后续会基于这些特征做进一步的特征工程操作
time_fea = ['hour', 'weekday', 'min', 'isnight', 'isholiday', 'is_not_work']

# 针对'time_fea'列表中的每一个特征列，进行以下操作：
# 计算在每个用户以及对应时间特征分组下，'diff_last_1'（每个用户上一次操作与当前操作的时间差）特征的平均值
# 通过分组变换（transform）操作，先按照['user_name', col]进行分组，然后在每个分组内计算平均值，结果的行数与原数据一致
for col in time_fea:
    data[col + '_diff1_mean_u'] = data.groupby(['user_name', col])['diff_last_1'].transform('mean')
    # 计算在每个用户以及对应时间特征分组下，'diff_last_1'特征的标准差，用于衡量每个分组内时间差的离散程度
    data[col + '_diff1_std_u'] = data.groupby(['user_name', col])['diff_last_1'].transform('std')

# 同样针对'time_fea'列表中的每一个特征列，进行以下操作：
# 计算在每个用户以及对应时间特征分组下，'diff_next'（每个用户下一次操作与当前操作的时间差）特征的平均值
for col in time_fea:
    data[col + '_diff1_next_mean_u'] = data.groupby(['user_name', col])['diff_next'].transform('mean')
    # 计算在每个用户以及对应时间特征分组下，'diff_next'特征的标准差，衡量每个分组内下一次操作时间差的离散程度
    data[col + '_diff1_next_std_u'] = data.groupby(['user_name', col])['diff_next'].transform('std')

# 删除'data'数据集中的'diff_next'列，可能是因为后续不再需要原始的这个特征列了，或者已经基于它完成了相关特征工程操作
del data['diff_next']

# 定义一个列名列表，包含了后续要提取出来进行临时操作的一些列名，涵盖了用户相关、设备相关、网络相关等多方面的特征以及日期时间列
cols = ['id_by_me', 'user_name', 'ip_transform', 'device_num_transform',
        'browser_version', 'browser', 'os_type', 'os_version', 'http_status_code', 'op_city',
        'log_system_transform', 'url', 'op_datetime']

# 从'data'数据集中提取出上述指定列的数据，赋值给'tmp'变量，方便后续在这个子集上进行分组聚合等操作
tmp = data[cols]

# 在'tmp'数据集中新增一列'op_day'，其值为'op_datetime'列对应的日期部分（去除时间信息，只保留年月日）
tmp['op_day'] = tmp['op_datetime'].dt.date

# 按照['user_name', 'op_day']进行分组，并对多个列进行聚合操作
# 这里将'id_by_me'、'ip_transform'等多个列的值聚合为列表形式，例如同一个用户在同一天内的多个'id_by_me'值会被放在一个列表中
tmp = tmp.groupby(['user_name', 'op_day'], as_index=False).agg(
    {'id_by_me': list, 'ip_transform': list, 'device_num_transform': list,
     'browser_version': list, 'browser': list, 'os_type': list, 'os_version': list, 'http_status_code': list,
     'op_city': list,
     'log_system_transform': list, 'url': list})

# 定义一个函数'get_which_time'，用于统计输入特征列表'fea'中每个元素出现的次数顺序列表
# 它首先创建一个以'col_unique'（唯一值列表）为键，初始值都为0的字典，然后遍历'fea'列表，每出现一次对应元素就在字典中其对应的值加1，
# 并将每次加1后的值依次添加到'count_list'列表中，最后返回这个记录出现次数顺序的列表
def get_which_time(col_unique, fea):
    fea_dict = dict.fromkeys(col_unique, 0)
    count_list = []
    for i in range(len(fea)):
        fea_dict[fea[i]] = fea_dict[fea[i]] + 1
        count_list.append(fea_dict[fea[i]])
    return count_list

# 使用'tqdm'库（通常用于显示进度条，方便在循环处理较长时间任务时查看进度）来遍历指定的列名列表
# 对于每个列，先获取该列在'data'数据集中的唯一值列表，然后在'tmp'数据集中新增一列，列名是原列名加上'_countls'后缀
# 新增列的值通过对原列应用匿名函数来计算，匿名函数内部调用'get_which_time'函数来统计每个元素在对应列中的出现次数顺序
for col in tqdm(['ip_transform', 'device_num_transform',
                 'browser_version', 'browser', 'os_type', 'os_version', 'http_status_code', 'op_city',
                 'log_system_transform', 'url']):
    col_unique = data[col].unique()
    tmp[col + '_countls'] = tmp[col].apply(lambda x: get_which_time(col_unique, x))

# 对'tmp'数据集进行'explode'操作，将列表形式的列（如'id_by_me'、'ip_transform'等以及新生成的带有'_countls'后缀的列）展开
# 展开后每一行对应原来列表中的一个元素，行数会相应增加，例如原来一行中某个列的值是包含3个元素的列表，展开后会变成3行
tmp = tmp.explode(['id_by_me', 'ip_transform',
                   'device_num_transform', 'browser_version', 'browser', 'os_type',
                   'os_version', 'http_status_code', 'op_city', 'log_system_transform',
                   'url', 'ip_transform_countls',
                   'device_num_transform_countls', 'browser_version_countls',
                   'browser_countls', 'os_type_countls', 'os_version_countls',
                   'http_status_code_countls', 'op_city_countls',
                   'log_system_transform_countls', 'url_countls'])

# 重置'tmp'数据集的索引，使其索引重新从0开始，连续且有序，一般在进行了一些可能打乱索引顺序的操作（如'explode'）后需要进行此操作
tmp = tmp.reset_index(drop=True)

# 定义一个列名列表，包含了要从临时数据集'tmp'中提取并合并到主数据集'data'中的列名，
# 这些列大多是之前经过一系列特征工程操作生成的带有'_countls'后缀的计数相关特征列，以及'id_by_me'作为关联键
cols = ['id_by_me', 'ip_transform_countls', 'device_num_transform_countls',
        'browser_version_countls', 'browser_countls', 'os_type_countls',
        'os_version_countls', 'http_status_code_countls', 'op_city_countls',
        'log_system_transform_countls', 'url_countls']

# 将主数据集'data'与临时数据集'tmp'中指定的列按照'id_by_me'列进行左连接合并，
# 这样'data'数据集就包含了来自'tmp'的相关特征信息，对于'tmp'中不存在对应'id_by_me'值的行，对应新合并的列会填充为缺失值
data = pd.merge(data, tmp[cols], on='id_by_me', how='left')

# 遍历所有以'_countls'后缀结尾的列名，将这些列的数据类型转换为整数类型，
# 因为这些列之前经过一些操作后可能是其他数据类型（比如对象类型等），转换为整数方便后续处理和模型使用
for col in ['ip_transform_countls', 'device_num_transform_countls',
            'browser_version_countls', 'browser_countls', 'os_type_countls',
            'os_version_countls', 'http_status_code_countls', 'op_city_countls',
            'log_system_transform_countls', 'url_countls']:
    data[col] = data[col].astype(int)

# 重新定义一个列名列表，包含了一些基础的与用户、设备、网络以及日期时间相关的列名，
# 后续会基于这些列再次进行特征工程操作，先提取出来存到临时变量'tmp'中方便操作
cols = ['id_by_me', 'user_name', 'ip_transform', 'device_num_transform', 'browser_version', 'browser', 'os_type',
        'os_version', 'http_status_code', 'op_city', 'log_system_transform', 'url', 'op_datetime']
tmp = data[cols]

# 账号最近几次登陆
# 循环30次（从1到29），针对每次循环：
# 在临时数据集'tmp'中创建一个新列，列名格式为'usr_diff_last_{x}'，用于表示每个用户在当前操作与过去第x次操作之间的时间差（以分钟为单位）
# 通过分组变换（groupby结合transform），按照'user_name'分组后对'op_datetime'列进行差分操作（diff(x)）获取时间差，再将总秒数转换为分钟数
for x in range(1, 30):
    tmp['usr_diff_last_' + str(x)] = tmp.groupby(['user_name'])['op_datetime'].transform(
        lambda i: i.diff(x)).dt.total_seconds() / 60
# 提取所有名称中包含'_diff_last_'的列名，组成一个列表，这些列都是刚刚生成的表示不同时间差的列，后续会进行处理
merge_cols = [col for col in tmp.columns if '_diff_last_' in col]
# 在临时数据集'tmp'中新增一列'ip_diff_list_30'，其值为包含所有时间差列值的列表形式，
# 通过将上述提取的时间差列的值转换为列表赋值给该列，方便后续统一处理这些时间差信息
tmp['ip_diff_list_30'] = tmp[merge_cols].values.tolist()
# 删除所有包含'_diff_last_'的列，因为已经将它们的值整合到了'ip_diff_list_30'列中，避免数据冗余，且后续可能不需要原始列了
tmp.drop(merge_cols, axis=1, inplace=True)

# 账号最近几次登陆对应ip
# 这里是两层嵌套循环，外层循环和内层循环都是从1到29，理论上可能是想针对每个过去的操作次数（最多到29次）以及对应的不同维度进行操作，
# 不过从逻辑上看可能存在重复循环的情况，或许外层循环控制一种主要逻辑，内层循环可以适当调整或优化，此处先按原代码逻辑注释
for x in range(1, 30):
    for x in range(1, 30):
        # 针对每个用户，对'ip_transform'列进行移位操作（shift(x)），即获取每个用户过去第x次操作对应的IP信息，
        # 并重置索引（去掉分组索引级别，只保留默认的整数索引），然后将结果赋值给新创建的列'usr_last_ip{x}'，
        # 这样每个新列就代表了过去某一次操作对应的IP地址
        shifted_series = tmp.groupby(['user_name'])['ip_transform'].shift(x).reset_index(level=0, drop=True)
        tmp['usr_last_ip' + str(x)] = shifted_series
# 提取所有名称中包含'_last_'的列名，组成一个列表，这些列都是刚刚生成的表示过去不同次操作对应IP地址的列，后续会进行处理
merge_cols = [col for col in tmp.columns if '_last_' in col]
# 在临时数据集'tmp'中新增一列'usr_ip_list_30'，其值为包含所有过去操作对应IP地址列值的列表形式，
# 通过将上述提取的IP地址列的值转换为列表赋值给该列，方便后续统一处理这些IP地址信息
tmp['usr_ip_list_30'] = tmp[merge_cols].values.tolist()
# 删除所有包含'_last_'的列，因为已经将它们的值整合到了'usr_ip_list_30'列中，避免数据冗余，且后续可能不需要原始列了
tmp.drop(merge_cols, axis=1, inplace=True)

# 定义一个函数'get_nunique_minute'，用于根据给定的时间差列表（diff_list）、对应唯一值列表（uni_list）以及时间限制（minute），
# 统计在时间限制内的唯一值数量。具体逻辑是遍历时间差列表，当时间差小于给定的分钟数限制时，将对应的唯一值添加到列表'ls'中，
# 最后返回这个列表中元素的唯一值数量（即去重后的元素个数）
def get_nunique_minute(diff_list, uni_list, minute):
    ls = []
    for i in range(len(diff_list)):
        if diff_list[i] < minute:
            ls.append(uni_list[i])
        else:
            break
    return pd.Series(ls).nunique()

# 在临时数据集'tmp'中新增一列'ip_time_nui_6'，其值通过对每行数据应用上述'get_nunique_minute'函数来计算，
# 传入的参数分别是'ip_diff_list_30'（包含时间差信息的列表）、'usr_ip_list_30'（包含对应IP地址信息的列表）以及时间限制60 * 6（即6小时对应的分钟数），
# 这样该列就表示在6小时内的IP地址的唯一值数量，用于刻画一定时间范围内IP地址的变化情况等特征
tmp['ip_time_nui_6'] = tmp.apply(lambda row: get_nunique_minute(row['ip_diff_list_30'], row['usr_ip_list_30'], 60 * 6),
                                 axis=1)

# 类似地，在临时数据集'tmp'中新增一列'ip_time_nui_12'，计算在12小时内的IP地址的唯一值数量，
# 传入的时间限制为60 * 12（即12小时对应的分钟数），用于从不同时间尺度上捕捉IP地址变化特征
tmp['ip_time_nui_12'] = tmp.apply(
    lambda row: get_nunique_minute(row['ip_diff_list_30'], row['usr_ip_list_30'], 60 * 12), axis=1)

# 同样，在临时数据集'tmp'中新增一列'ip_time_nui_24'，计算在24小时内的IP地址的唯一值数量，
# 传入的时间限制为60 * 24（即24小时对应的分钟数），进一步丰富不同时间范围下IP地址变化相关的特征信息
tmp['ip_time_nui_24'] = tmp.apply(
    lambda row: get_nunique_minute(row['ip_diff_list_30'], row['usr_ip_list_30'], 60 * 24), axis=1)

# 提取所有名称中包含'ip_time_nui_'的列名，组成一个列表，这些列都是刚刚生成的表示不同时间范围内IP地址唯一值数量的特征列，
# 后续会将这些列合并到主数据集'data'中，用于模型训练等操作
cols = [col for col in tmp.columns if 'ip_time_nui_' in col]

# 将'id_by_me'列名添加到列名列表中，因为'id_by_me'是用于合并操作的关联键，
# 后续要基于这个键将临时数据集'tmp'中的相关特征列合并到主数据集'data'中
cols.append('id_by_me')

# 将主数据集'data'与临时数据集'tmp'中指定的列（包含不同时间范围IP地址唯一值数量特征列以及'id_by_me'列）按照'id_by_me'列进行左连接合并，
# 使得主数据集'data'包含这些新生成的有价值的特征信息，方便后续机器学习模型使用这些特征进行训练和预测等操作
data = pd.merge(data, tmp[cols], on='id_by_me', how='left')

# 初始化一个空列表'cross_cols'，从后续代码推测可能是用于存储一些交叉组合特征列名相关的信息，
cross_cols = []

# department_city
# 将'department'列的数据转换为字符串类型后与'op_city'列（同样转换为字符串类型）进行字符串拼接，
# 生成一个新的特征列'department_op_city'，这样的交叉组合特征可能有助于捕捉部门与操作城市之间的关联关系，
# 例如不同部门在不同城市的操作模式等信息，供后续机器学习模型挖掘潜在规律
data['department_op_city'] = data['department'].astype(str) + data['op_city'].astype(str)
# 将新生成的交叉组合特征列名'department_op_city'添加到'cross_cols'列表中，
# 从后续代码推测这个列表可能用于统一管理这些新生成的交叉特征列名，方便后续操作（比如可能的编码处理等）
cross_cols.append('department_op_city')

# department_log_system_transform
# 将'department'列转换为字符串后与'log_system_transform'列（也转换为字符串）进行拼接，
# 创建一个新特征列'department_log_system_transform'
# 有助于模型发现不同部门在日志系统相关方面的不同行为模式
data['department_log_system_transform'] = data['department'].astype(str) + data['log_system_transform'].astype(str)
# 此处有一行被注释掉的代码，原计划可能是使用'label_encoder'（应该是一个自定义或导入的编码函数/工具）对这个新特征列进行编码处理，
# 可能是想将其转换为数值型以便机器学习模型更好地处理，但当前未执行该操作
# data['department_log_system_transform'] = label_encoder(data['department_log_system_transform'])
# 将新生成的特征列名添加到'cross_cols'列表中，用于后续管理
cross_cols.append('department_log_system_transform')

# browser_version_op_city
# 把'browser_version'列转换为字符串与'op_city'列（字符串类型）进行拼接，
# 生成新特征列'browser_version_op_city'，用于捕捉浏览器版本与操作城市之间的关联，
# 例如某些浏览器版本在特定城市可能更常用或者出现特定行为模式等情况
data['browser_version_op_city'] = data['browser_version'].astype(str) + data['op_city'].astype(str)
# data['browser_version_op_city'] = label_encoder(data['browser_version_op_city'])
# 将列名添加到'cross_cols'列表
cross_cols.append('browser_version_op_city')

# browser_op_city
# 将'browser'列转换为字符串后和'op_city'列拼接，创建新特征列'browser_op_city'，
# 以此来体现浏览器与操作城市之间的关系，不同浏览器在不同城市的使用频率、相关操作行为等差异可通过该特征反映
data['browser_op_city'] = data['browser'].astype(str) + data['op_city'].astype(str)
# 注释掉的编码相关操作，可能是要对该特征列进行编码转换，使其适合模型处理
# data['browser_op_city'] = label_encoder(browser_op_city')
# 添加列名到'cross_cols'列表
cross_cols.append('browser_op_city')

# browser_log_system_transform
# 把'browser'列（转换为字符串）与'log_system_transform'列（同样转换为字符串）拼接，
# 生成新特征列'browser_log_system_transform'，用于表示浏览器与日志系统转换之间的联系，
# 比如不同浏览器在日志记录、系统交互等方面的差异情况
data['browser_log_system_transform'] = data['browser'].astype(str) + data['log_system_transform'].astype(str)
# 被注释的编码操作，可能是想将该特征进行编码处理以便模型使用
# data['browser_log_system_transform'] = label_encoder(data['browser_log_system_transform'])
# 将列名添加到'cross_cols'列表
cross_cols.append('browser_log_system_transform')

# os_type_op_city
# 将'os_type'列转换为字符串与'op_city'列拼接，创建新特征列'os_type_op_city'，
# 该特征有助于捕捉操作系统类型与操作城市之间的关联，例如某些操作系统在特定城市更流行或有不同的操作行为特点
data['os_type_op_city'] = data['os_type'].astype(str) + data['op_city'].astype(str)
# 注释掉的编码操作，原计划可能是对这个新特征进行编码转换
# data['os_type_op_city'] = label_encoder(data['os_type_op_city'])
# 添加列名到'cross_cols'列表
cross_cols.append('os_type_op_city')

# os_type_log_system_transform
# 把'os_type'列（转换为字符串）与'log_system_transform'列（转换为字符串）进行拼接，
# 生成新特征列'os_type_log_system_transform'，用于体现操作系统类型与日志系统转换之间的关联，
# 不同操作系统在日志处理、系统交互方面的差异可通过该特征展现给模型
data['os_type_log_system_transform'] = data['os_type'].astype(str) + data['log_system_transform'].astype(str)
# 被注释的编码操作，可能是要对该特征进行编码使其适合模型处理
# data['os_type_log_system_transform'] = label_encoder(data['os_type_log_system_transform'])
# 将列名添加到'cross_cols'列表
cross_cols.append('os_type_log_system_transform')

# os_version_op_city
# 将'os_version'列转换为字符串后与'op_city'列拼接，创建新特征列'os_version_op_city'，
# 此特征可用于捕捉操作系统版本与操作城市之间的联系，比如不同版本在不同城市的使用情况、相关操作行为等
data['os_version_op_city'] = data['os_version'].astype(str) + data['op_city'].astype(str)
# 注释掉的编码操作，可能是计划对该特征列进行编码转换
# data['os_version_op_city'] = label_encoder(data['os_version_op_city'])
# 添加列名到'cross_cols'列表
cross_cols.append('os_version_op_city')

# os_type_log_system_transform
# 是将'os_type'列与'log_system_transform'列进行字符串拼接，生成体现两者关联的特征列
data['os_type_log_system_transform'] = data['os_type'].astype(str) + data['log_system_transform'].astype(str)
# 同样被注释的编码操作，可能是想将该特征转换为适合模型处理的数值型数据
# data['os_type_log_system_transform'] = label_encoder(data['os_type_log_system_transform'])
# 将列名添加到'cross_cols'列表
cross_cols.append('os_type_log_system_transform')

# op_city_log_system_transform
# 将'op_city'列（转换为字符串）与'log_system_transform'列（转换为字符串）拼接，
# 生成新特征列'op_city_log_system_transform'，用于体现操作城市与日志系统转换之间的关系，
# 例如不同城市在日志系统相关操作上的差异特点等信息可通过该特征反映出来
data['op_city_log_system_transform'] = data['op_city'].astype(str) + data['log_system_transform'].astype(str)
# 被注释的编码操作，可能是要对该特征进行编码使其符合模型输入要求
# data['op_city_log_system_transform'] = label_encoder(data['op_city_log_system_transform'])
# 将列名添加到'cross_cols'列表
cross_cols.append('op_city_log_system_transform')

# departmen_url
# 这里又重复创建了'op_city_log_system_transform'特征列，同样是将'department'列与'log_system_transform'列拼接
data['op_city_log_system_transform'] = data['department'].astype(str) + data['log_system_transform'].astype(str)
# 被注释的编码操作，可能是要对这个新（但重复定义的）特征列进行编码处理
# data['op_city_log_system_transform'] = label_encoder(data['op_city_log_system_transform'])
# 将列名添加到'cross_cols'列表
cross_cols.append('op_city_log_system_transform')

# 定义一个列名列表，包含了多个与IP、设备、浏览器、操作系统、网络相关等方面的特征列名，
# 后续会基于这些列进行分组统计操作，以生成新的特征
cols = ['ip_transform', 'device_num_transform',
        'browser_version', 'browser', 'os_type', 'os_version',
        'http_status_code', 'op_city', 'log_system_transform', 'url']

# 针对上述定义的列名列表中的每一个列，进行以下操作：
# 先从'data'数据集中筛选出训练集部分（通过判断'istest'列等于0来确定），然后按照['user_name', col, 'hour']进行分组，
# 统计每个分组内'is_risk'列（推测是风险标识列）的数量，也就是计算在每个用户、对应特征以及小时维度下的风险出现次数，
# 最后将结果重置索引，使其成为一个常规的数据框结构，方便后续合并操作
for col in cols:
    tmp = data[data['istest'] == 0].groupby(['user_name', col, 'hour'])['is_risk'].count().reset_index()
    tmp.columns = ['user_name', col, 'hour', col + '_hour_count']
    data = pd.merge(data, tmp, on=['user_name', col, 'hour'], how='left')

tmp = data[data['istest'] == 0].groupby(['user_name', 'is_not_work', 'hour'], as_index=False)['is_risk'].agg(
    {'work_hour_count': 'count'})
data = pd.merge(data, tmp, how='left', on=['user_name', 'is_not_work', 'hour'])

date_fea = ['weekday', 'isholiday']

for col in date_fea:
    tmp = data[data['istest'] == 0].groupby(['user_name', col, 'hour'], as_index=False)['is_risk'].agg(
        {col + '_count': 'count'})
    data = pd.merge(data, tmp, how='left', on=['user_name', col, 'hour'])

    numeric_features = data.select_dtypes(include=[np.number])
    categorical_features = data.select_dtypes(include=[object])

    data[categorical_features.columns] = ce.OrdinalEncoder().fit_transform(data[categorical_features.columns])

    train_data = data[data['istest'] == 0]
    test = data[data['istest'] == 1]

    test = test.sort_values('id').reset_index(drop=True)

    train = train_data[train_data['op_datetime'] < '2022-04-01'].reset_index(drop=True)
    val = train_data[train_data['op_datetime'] >= '2022-04-01'].reset_index(drop=True)

    fea = [col for col in data.columns if
           col not in ['id', 'index', 'id_by_me', 'op_datetime', 'op_day', 'day', 'op_month', 'is_risk', 'ts', 'ts1',
                       'ts2',
                       'diff_next']]

    x_train = train[fea]
    y_train = train['is_risk']

    x_val = val[fea]
    y_val = val['is_risk']

    x_test = test[fea]
    y_test = test['is_risk']

    # LightGBM模型相关
    importance = 0
    pred_y_lgb = pd.DataFrame()
    var_pre_lgb = pd.DataFrame()
    lgb_score_list = []  # 用于存储每次折叠验证集上的AUC得分
    lgb_mse_list = []  # 用于存储每次折叠验证集上的均方误差

    seeds = 2022

    params_lgb = {
        'learning_rate': 0.05,
        'boosting_type': 'gbdt',
        'objective': 'binary',
        'metric': 'auc',
        'num_leaves': 64,
        'verbose': -1,
        'seed': 2022,
        'n_jobs': -1,

        'feature_fraction': 0.8,
        'bagging_fraction': 0.9,
        'bagging_freq': 4,
        # 'min_child_weight': 10,
        "min_data_in_leaf": 20
    }

    kf = KFold(n_splits=5, shuffle=True, random_state=2022)
    for i, (train_idx, val_idx) in enumerate(kf.split(x_train, y_train)):
        print(
            '************************************ {} {}************************************'.format(str(i + 1),
                                                                                                    str(seeds)))
        trn_x, trn_y, val_x, val_y = x_train.iloc[train_idx], y_train.iloc[train_idx], x_train.iloc[val_idx], \
        y_train.iloc[
            val_idx]
        train_data = lgb.Dataset(trn_x,
                                 trn_y)
        val_data = lgb.Dataset(val_x,
                               val_y)
        model = lgb.train(params_lgb, train_data, valid_sets=[val_data], num_boost_round=20000,
                          callbacks=[lgb.early_stopping(100), lgb.log_evaluation(2000)])

        pred_y_lgb['fold_%d_seed_%d' % (i, seeds)] = model.predict(x_test)
        var_pre_lgb['fold_%d_seed_%d' % (i, seeds)] = model.predict(x_val)

        # 计算LightGBM在验证集上的AUC得分并添加到列表
        lgb_score_list.append(auc(val_y, model.predict(val_x)))
        # 计算LightGBM在验证集上的均方误差并添加到列表
        lgb_mse_list.append(mean_squared_error(val_y, model.predict(val_x)))

        importance += model.feature_importance(importance_type='gain') / 5

    # 计算LightGBM模型平均AUC和平均均方误差
    lgb_mean_auc = np.mean(lgb_score_list)
    lgb_mean_mse = np.mean(lgb_mse_list)
    print("LightGBM模型平均AUC:", lgb_mean_auc)
    print("LightGBM模型平均均方误差:", lgb_mean_mse)

    # 随机森林模型相关
    pred_y_rf = pd.DataFrame()
    rf_score_list = []  # 用于存储每次折叠验证集上的AUC得分
    rf_mse_list = []  # 用于存储每次折叠验证集上的均方误差
    seeds_rf = 2022  # 可以设置不同的随机种子
    rf_model = RandomForestClassifier(n_estimators=100, random_state=seeds_rf, n_jobs=-1)
    kf_rf = KFold(n_splits=5, shuffle=True, random_state=seeds_rf)
    for i, (train_idx, val_idx) in enumerate(kf_rf.split(x_train, y_train)):
        print(
            '************************************ RF Fold {} {}************************************'.format(str(i + 1),
                                                                                                            str(seeds_rf)))
        trn_x_rf, trn_y_rf, val_x_rf, val_y_rf = x_train.iloc[train_idx], y_train.iloc[train_idx], x_train.iloc[
            val_idx], y_train.iloc[val_idx]
        rf_model.fit(trn_x_rf, trn_y_rf)
        pred_y_rf['fold_%d_seed_%d' % (i, seeds_rf)] = rf_model.predict_proba(x_test)[:, 1]  # 取正类概率
        # 计算随机森林在验证集上的AUC得分并添加到列表
        rf_score_list.append(auc(val_y_rf, rf_model.predict_proba(val_x_rf)[:, 1]))
        # 计算随机森林在验证集上的均方误差并添加到列表
        rf_mse_list.append(mean_squared_error(val_y_rf, rf_model.predict_proba(val_x_rf)[:, 1]))
    #输出随机森林整体的特征重要性情况
    importances = rf_model.feature_importances_
    std = np.std([tree.feature_importances_ for tree in rf_model.estimators_], axis=0)
    indices = np.argsort(importances)[::-1]
    print("Feature ranking:")
    for f in range(len(x_train.columns)):
        print("%d. feature %s (%f)" % (f + 1, x_train.columns[indices[f]], importances[indices[f]]))

    # 计算随机森林模型平均AUC和平均均方误差
    rf_mean_auc = np.mean(rf_score_list)
    rf_mean_mse = np.mean(rf_mse_list)
    print("随机森林模型平均AUC:", rf_mean_auc)
    print("随机森林模型平均均方误差:", rf_mean_mse)

    # XGBoost模型相关
    pred_y_xgb = pd.DataFrame()
    xgb_score_list = []  # 用于存储每次折叠验证集上的AUC得分
    xgb_mse_list = []  # 用于存储每次折叠验证集上的均方误差
    seeds_xgb = 2022  # 设置随机种子，可按需调整

    params_xgb = {
        'learning_rate': 0.05,
        'max_depth': 6,
        'subsample': 0.8,
        'colsample_bytree': 0.8,
        'objective': 'binary:logistic',
        'eval_metric': 'auc',  # 设置评估指标为AUC
        'seed': seeds_xgb,
        'n_jobs': -1
    }

    kf_xgb = KFold(n_splits=5, shuffle=True, random_state=seeds_xgb)
    for i, (train_idx, val_idx) in enumerate(kf_xgb.split(x_train, y_train)):
        print(
            '************************************ XGBoost Fold {} {}************************************'.format(str(i + 1),
                                                                                                                str(seeds_xgb)))
        trn_x, trn_y, val_x, val_y = x_train.iloc[train_idx], y_train.iloc[train_idx], x_train.iloc[val_idx], \
        y_train.iloc[val_idx]
        train_data_xgb = xgb.DMatrix(trn_x, label=trn_y)  # 使用DMatrix数据结构
        val_data_xgb = xgb.DMatrix(val_x, label=val_y)
        model_xgb = xgb.train(params_xgb, train_data_xgb, num_boost_round=20000, evals=[(val_data_xgb, "val")],
                              early_stopping_rounds=100)  # 设置早停轮数

        pred_y_xgb['fold_%d_seed_%d' % (i, seeds_xgb)] = model_xgb.predict(xgb.DMatrix(x_test))
        # 计算XGBoost在验证集上的AUC得分并添加到列表
        xgb_score_list.append(auc(val_y, model_xgb.predict(xgb.DMatrix(val_x))))
        # 计算XGBoost在验证集上的均方误差并添加到列表
        xgb_mse_list.append(mean_squared_error(val_y, model_xgb.predict(xgb.DMatrix(val_x))))

    # 计算XGBoost模型平均AUC和平均均方误差
    xgb_mean_auc = np.mean(xgb_score_list)
    xgb_mean_mse = np.mean(xgb_mse_list)
    print("XGBoost模型平均AUC:", xgb_mean_auc)
    print("XGBoost模型平均均方误差:", xgb_mean_mse)

    # 定义一个字典来存储模型的性能指标
    model_performance = {
        'LightGBM': {'AUC': lgb_mean_auc, 'MSE': lgb_mean_mse},
        'Random Forest': {'AUC': rf_mean_auc, 'MSE': rf_mean_mse},
        'XGBoost': {'AUC': xgb_mean_auc, 'MSE': xgb_mean_mse}
    }

    # 提取模型名称和对应的AUC、MSE值用于可视化
    models = list(model_performance.keys())
    aucs = [model_performance[model]['AUC'] for model in models]
    mses = [model_performance[model]['MSE'] for model in models]

    # 设置图形大小
    plt.figure(figsize=(10, 6))

    # 绘制AUC对比柱状图
    plt.subplot(1, 2, 1)
    sns.barplot(x=models, y=aucs)
    plt.title('Model AUC Comparison')
    plt.xlabel('Model')
    plt.ylabel('AUC')

    # 绘制MSE对比柱状图
    plt.subplot(1, 2, 2)
    sns.barplot(x=models, y=mses)
    plt.title('Model MSE Comparison')
    plt.xlabel('Model')
    plt.ylabel('MSE')

    # 调整布局，显示图形
    plt.tight_layout()
    plt.show()
    # 选择模型的逻辑
    best_model = None
    best_auc = -float('inf')
    best_mse = float('inf')

    for model, metrics in model_performance.items():
        if metrics['AUC'] > best_auc and metrics['MSE'] < best_mse:
            best_model = model
            best_auc = metrics['AUC']
            best_mse = metrics['MSE']

    if best_model:
        print(f"{best_model}模型整体表现更好")
        # 根据选择的模型设置预测结果
        if best_model == 'LightGBM':
            pred_y = pred_y_lgb.mean(axis=1).to_frame(name='is_risk')
        elif best_model == 'Random Forest':
            pred_y = pred_y_rf.mean(axis=1).to_frame(name='is_risk')
        else:
            pred_y = pred_y_xgb.mean(axis=1).to_frame(name='is_risk')
    else:
        print("三个模型各有优劣，难以简单判断哪个更好，可根据具体需求进一步分析")

    test['is_risk'] = pred_y.values

    df_test = pd.read_csv('data/evaluation_public.csv')
    df_test = pd.merge(df_test, test[['id', 'is_risk']], how='left')
    df_test['op_datetime'] = pd.to_datetime(df_test['op_datetime'])
    df_test = df_test.sort_values('op_datetime').reset_index(drop=True)
    df_test['hour'] = df_test['op_datetime'].dt.hour
    df_test.loc[df_test['hour'] < 6, 'is_risk'] = 1
    df_test.loc[df_test['hour'] > 20, 'is_risk'] = 1

    df_test[['id', 'is_risk']].to_csv("result/result3.csv", index=False)